LBB

优化 Dockerfile 体积及构建速度

# 优化 Dockerfile 体积及构建速度 以 Nuxt 的基础项目 `npx nuxi init nuxt-app`,来介绍如何修改 Dockerfile 优化镜像体积及构建速度。 示例 demo 可以在 https://github.com/lbb00/docker-optimize-demo 查看。 效果: ![img](../media/4c2c3a70-3a73-4648-9049-00621b97cac7.png) ## Dockerfile.0 一个基础的 Nuxt Dockerfile ```dockerfile FROM node:16 EXPOSE 3000 WORKDIR /app COPY . . RUN yarn install --frozen-lockfile RUN yarn build ENTRYPOINT ["node"] CMD [".output/server/index.mjs"] ``` 在这个 Dockerfile 中,仅有最基本的安装依赖、构建。当执行完`docker build -f Dockerfile.0 . -t demo:v0`后,发现这个简单的 Image 足足有 1.52GB 的大小。 通过`docker history demo:v0`获取到 Image 的构建历史,获得到下图: ![img](../media/1e355c85-68d9-4187-8075-e48ddd3cbc90.png) 分析上图可知在 COMMENT 来自于 Dockerfile.0 之前就已经非常大了,这些内容来自于`FROM node:16`。 通过`docker pull node:16 && docker images node:16`,可以看到 node:16 这个基础镜像体积有 850MB。 ![img](../media/0c8abfdc-31f6-4d5c-adf9-205b39404eb4.png) ## Dockerfile.1 使用体积更小更精简的基础镜像 在 Dockerfile.0 中发现 node:16 的基础镜像体积非常大,可以使用更小的 Node 镜像来代替。 - `node:<version>` 基于 Debian 的官方镜像 - `node:<version>-slim` 删除冗余依赖后的精简版本镜像,同样是基于 debian 构建,体积上比默认镜像小很多,删除了很多公共的软件包,只保留了最基本的 node 运行环境 - `node:<version>-alpine` 基于 Alpine 镜像构建 对比`docker pull node:16 && docker pull node:16-buster-slim && docker pull node:16-alpine && docker images | grep node`,node:16-alpine 体积是最小的。 ![img](../media/28b653b4-1644-4cb9-a7b6-274a2d5d73cb.png) Dockerfile.1 中把 `FROM node:16` 修改为 `FROM node:16-alpine` 后构建,镜像体积下降到 773MB。 > alpine 不是最佳选择,这里只是举例,建议使用 `node:<version>-slim` 或 `gcr.io/distroless/nodejs` ## Dockerfile.2 多阶段构建 对 Dockerfile.1 构建出的镜像执行`docker history demo:v1` ,得到下图: ![img](../media/0a4b5d0f-0933-49bf-9940-d20176bdd8f0.png) 现在占用体积比较大的还有 `RUN yarn install --frozen-lockfile` 这一层,node_modules 占用了不少的体积。大部分的 Node 开发场景中,项目在执行完 build 以后,仅需要一些生产时才需要用到的依赖,甚至通过一些打包工具后完全不需要任何来自 node_modules 的依赖。 在 build 完成以后,删除 node_modules,仅安装生产时所需要的依赖。但是由于 Image 中仍然包含了安装全部依赖的层,可以通过多阶段构建避免将没有必要的层写入到 Image 中。 在 Dockerfile.2 中,修改为: ```dockerfile FROM node:16-alpine as builder WORKDIR /app COPY . . RUN yarn install --frozen-lockfile RUN yarn build RUN rm -rf ./node_modules && yarn install --frozen-lockfile --production --ignore-scripts FROM node:16-alpine EXPOSE 80 WORKDIR /app COPY --from=builder /app . ENTRYPOINT ["node"] CMD [".output/server/index.mjs"] ``` 使用 Dockerfile.2 构建后,镜像体积仅有 112MB ## Dockerfile.3 利用 Docker layer cache 加速构建 关于 Docker 中层和缓存的概念在这里不在过多阐述。 在上述的 Dockerfile 中,如果修改了项目中的任何文件,甚至是 README.md,都会导致 Docker 从头开始构建,不能有效的利用缓存。特别是在 Node 项目中,项目经过一段时间迭代以后,依赖会越来越多,安装依赖也会成为构建过程中非常耗时的阶段之一。 因此在编写 Dockerfile 的时候可以遵循以下几点: - 将不会变动或极少变动的内容尽可能放到顶部 - 仅 COPY 当前阶段所需要的文件 以本项目举例 - yarn install 的过程仅需要 package.json、yarn.lock - build 时仅需要 tsconfig.json、app.vue、nuxt.config.ts - 生成最终镜像的阶段也仅需要从 builder 那里拷贝 node_modules 和 .output 两个文件夹 在 Dockerfile.3 中,修改为: ```dockerfile FROM node:16-alpine as builder WORKDIR /app COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile COPY tsconfig.json app.vue nuxt.config.ts ./ RUN yarn build RUN rm -rf ./node_modules RUN yarn install --frozen-lockfile --production --ignore-scripts FROM node:16-alpine EXPOSE 3000 WORKDIR /app COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/.output . ENTRYPOINT ["node"] CMD ["./server/index.mjs"] ``` 这样,只有 package.json、yarn.lock 文件发生了改动,构建时才会重新执行安装依赖。同理,只有 tsconfig.json、app.vue、nuxt.config.ts 发生了改动,才会重新执行构建,并且不会导致重新安装依赖。 ## 推荐工具 - [Dive](https://github.com/wagoodman/dive) 一种用于探索 docker image、层和发现缩小 Docker/OCI 图像大小的工具。 ## 参考 [1] [10 best practices to containerize Node.js web applications with Docker](https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker) [2] [3 simple tricks for smaller Docker images](https://learnk8s.io/blog/smaller-docker-images) [3] [Choosing the best Node.js Docker image](https://snyk.io/blog/choosing-the-best-node-js-docker-image) [4] [Docker and Node.js Best Practices](https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#handling-kernel-signals)
优化 Dockerfile 体积及构建速度 | LBB